package org.springframework.web.servlet.mvc.method.annotation;

import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.common.RequestAttrs;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.CoRequestMappingInfo;
import org.springframework.web.servlet.mvc.method.CoRequestMappingInfoHandlerMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;

import javax.servlet.http.HttpServletRequest;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * @author float.lu
 */
public class CoRequestMappingHandlerMapping  extends CoRequestMappingInfoHandlerMapping
        implements EmbeddedValueResolverAware {

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {

       if (request.getAttribute(RequestAttrs.CO_MARK) != null){
            String moduleMapping = (String)request.getAttribute(RequestAttrs.CO_CURRENT_MODULE_MARK);
            if (moduleMapping != null) {
                HandlerMethod handlerMethod = super.lookupHandlerMethod(moduleMapping, request);
                return handlerMethod;
            }
        } else {
            request.setAttribute(RequestAttrs.PAGE_KEY, lookupPath);
            return super.lookupHandlerMethod(lookupPath, request);
        }
        return null;
    }


    private boolean useSuffixPatternMatch = true;

    private boolean useRegisteredSuffixPatternMatch = false;

    private boolean useTrailingSlashMatch = true;

    private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();

    private StringValueResolver embeddedValueResolver;

    private CoRequestMappingInfo.BuilderConfiguration config = new CoRequestMappingInfo.BuilderConfiguration();


    /**
     * Whether to use suffix pattern match (".*") when matching patterns to
     * requests. If enabled a method mapped to "/users" also matches to "/users.*".
     * <p>The default value is {@code true}.
     * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for
     * more fine-grained control over specific suffixes to allow.
     */
    public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
        this.useSuffixPatternMatch = useSuffixPatternMatch;
    }

    /**
     * Whether to use suffix pattern match for registered file extensions only
     * when matching patterns to requests.
     * <p>If enabled, a controller method mapped to "/users" also matches to
     * "/users.json" assuming ".json" is a file extension registered with the
     * provided {@link #setContentNegotiationManager(ContentNegotiationManager)
     * contentNegotiationManager}. This can be useful for allowing only specific
     * URL extensions to be used as well as in cases where a "." in the URL path
     * can lead to ambiguous interpretation of path variable content, (e.g. given
     * "/users/{user}" and incoming URLs such as "/users/john.j.joe" and
     * "/users/john.j.joe.json").
     * <p>If enabled, this flag also enables
     * {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch}. The
     * default value is {@code false}.
     */
    public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
        this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
        this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
    }

    /**
     * Whether to match to URLs irrespective of the presence of a trailing slash.
     * If enabled a method mapped to "/users" also matches to "/users/".
     * <p>The default value is {@code true}.
     */
    public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
        this.useTrailingSlashMatch = useTrailingSlashMatch;
    }

    /**
     * Set the {@link ContentNegotiationManager} to use to determine requested media types.
     * If not set, the default constructor is used.
     */
    public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
        Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null");
        this.contentNegotiationManager = contentNegotiationManager;
    }

    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        this.embeddedValueResolver  = resolver;
    }

    @Override
    public void afterPropertiesSet() {
        this.config = new CoRequestMappingInfo.BuilderConfiguration();
        this.config.setPathHelper(getUrlPathHelper());
        this.config.setPathMatcher(getPathMatcher());
        this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
        this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
        this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
        this.config.setContentNegotiationManager(getContentNegotiationManager());

        super.afterPropertiesSet();
    }


    /**
     * Whether to use suffix pattern matching.
     */
    public boolean useSuffixPatternMatch() {
        return this.useSuffixPatternMatch;
    }

    /**
     * Whether to use registered suffixes for pattern matching.
     */
    public boolean useRegisteredSuffixPatternMatch() {
        return this.useRegisteredSuffixPatternMatch;
    }

    /**
     * Whether to match to URLs irrespective of the presence of a trailing slash.
     */
    public boolean useTrailingSlashMatch() {
        return this.useTrailingSlashMatch;
    }

    /**
     * Return the configured {@link ContentNegotiationManager}.
     */
    public ContentNegotiationManager getContentNegotiationManager() {
        return this.contentNegotiationManager;
    }

    /**
     * Return the file extensions to use for suffix pattern matching.
     */
    public List<String> getFileExtensions() {
        return this.config.getFileExtensions();
    }


    /**
     * {@inheritDoc}
     * Expects a handler to have a type-level @{@link Controller} annotation.
     */
    @Override
    protected boolean isHandler(Class<?> beanType) {
        return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
                (AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));
    }

    /**
     * Uses method and type-level @{@link RequestMapping} annotations to create
     * the RequestMappingInfo.
     * @return the created RequestMappingInfo, or {@code null} if the method
     * does not have a {@code @RequestMapping} annotation.
     * @see #getCustomMethodCondition(Method)
     * @see #getCustomTypeCondition(Class)
     */
    @Override
    protected CoRequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        CoRequestMappingInfo info = createRequestMappingInfo(method);
        if (info != null) {
            CoRequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                info = typeInfo.combine(info);
            }
        }
        return info;
    }

    /**
     * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)},
     * supplying the appropriate custom {@link RequestCondition} depending on whether
     * the supplied {@code annotatedElement} is a class or method.
     * @see #getCustomTypeCondition(Class)
     * @see #getCustomMethodCondition(Method)
     */
    private CoRequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        RequestCondition<?> condition = (element instanceof Class<?> ?
                getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
        return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
    }

    /**
     * Provide a custom type-level request condition.
     * The custom {@link RequestCondition} can be of any type so long as the
     * same condition type is returned from all calls to this method in order
     * to ensure custom request conditions can be combined and compared.
     * <p>Consider extending {@link AbstractRequestCondition} for custom
     * condition types and using {@link CompositeRequestCondition} to provide
     * multiple custom conditions.
     * @param handlerType the handler type for which to create the condition
     * @return the condition, or {@code null}
     */
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return null;
    }

    /**
     * Provide a custom method-level request condition.
     * The custom {@link RequestCondition} can be of any type so long as the
     * same condition type is returned from all calls to this method in order
     * to ensure custom request conditions can be combined and compared.
     * <p>Consider extending {@link AbstractRequestCondition} for custom
     * condition types and using {@link CompositeRequestCondition} to provide
     * multiple custom conditions.
     * @param method the handler method for which to create the condition
     * @return the condition, or {@code null}
     */
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return null;
    }

    /**
     * Create a {@link RequestMappingInfo} from the supplied
     * {@link RequestMapping @RequestMapping} annotation, which is either
     * a directly declared annotation, a meta-annotation, or the synthesized
     * result of merging annotation attributes within an annotation hierarchy.
     */
    protected CoRequestMappingInfo createRequestMappingInfo(
            RequestMapping requestMapping, RequestCondition<?> customCondition) {

        return CoRequestMappingInfo
                .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
                .methods(requestMapping.method())
                .params(requestMapping.params())
                .headers(requestMapping.headers())
                .consumes(requestMapping.consumes())
                .produces(requestMapping.produces())
                .mappingName(requestMapping.name())
                .customCondition(customCondition)
                .options(this.config)
                .build();
    }

    /**
     * Resolve placeholder values in the given array of patterns.
     * @return a new array with updated patterns
     */
    protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
        if (this.embeddedValueResolver == null) {
            return patterns;
        }
        else {
            String[] resolvedPatterns = new String[patterns.length];
            for (int i = 0; i < patterns.length; i++) {
                resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
            }
            return resolvedPatterns;
        }
    }

    @Override
    protected CorsConfiguration initCorsConfiguration(Object handler, Method method, CoRequestMappingInfo mappingInfo) {
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        CrossOrigin typeAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), CrossOrigin.class);
        CrossOrigin methodAnnotation = AnnotationUtils.findAnnotation(method, CrossOrigin.class);

        if (typeAnnotation == null && methodAnnotation == null) {
            return null;
        }

        CorsConfiguration config = new CorsConfiguration();
        updateCorsConfig(config, typeAnnotation);
        updateCorsConfig(config, methodAnnotation);

        if (CollectionUtils.isEmpty(config.getAllowedOrigins())) {
            config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS));
        }
        if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
            for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
                config.addAllowedMethod(allowedMethod.name());
            }
        }
        if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
            config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS));
        }
        if (config.getAllowCredentials() == null) {
            config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS);
        }
        if (config.getMaxAge() == null) {
            config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE);
        }
        return config;
    }

    private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) {
        if (annotation == null) {
            return;
        }
        for (String origin : annotation.origins()) {
            config.addAllowedOrigin(origin);
        }
        for (RequestMethod method : annotation.methods()) {
            config.addAllowedMethod(method.name());
        }
        for (String header : annotation.allowedHeaders()) {
            config.addAllowedHeader(header);
        }
        for (String header : annotation.exposedHeaders()) {
            config.addExposedHeader(header);
        }

        String allowCredentials = annotation.allowCredentials();
        if ("true".equalsIgnoreCase(allowCredentials)) {
            config.setAllowCredentials(true);
        } else if ("false".equalsIgnoreCase(allowCredentials)) {
            config.setAllowCredentials(false);
        } else if (!allowCredentials.isEmpty()) {
            throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", "
                    + "or an empty string (\"\"); current value is [" + allowCredentials + "].");
        }

        if (annotation.maxAge() >= 0 && config.getMaxAge() == null) {
            config.setMaxAge(annotation.maxAge());
        }
    }
}
